#! /usr/bin/env python

import os, sys, shutil, time, traceback
import urllib2
import flickrapi
import sqlite3

apiKey      = 'e597cdeedb619f3f47ca999cd6f42149'
apiSecret   = '578a2ab88db7e31b'

class NotConnectedException(Exception):
    def __init__(self, db):
        self.db = db

    def __str__(self):
        return self.db

class FlickrWatcherDb():
    def __init__(self, db='flickrwatcher.db'):
        try:
            self.db     = db
            self.dbconn = sqlite3.connect(db)
            self.cursor = self.dbconn.cursor()

            self.cursor.execute("""CREATE TABLE IF NOT EXISTS subscriptions
            (
                userName        text,
                contactName     text,
                contactId       text,
                subscribed      integer,
                maxPhotos       integer,
                UNIQUE
                (
                    userName,
                    contactId
                ) ON CONFLICT REPLACE
            )""")

            self.cursor.execute("""CREATE TABLE IF NOT EXISTS watch
            (
                userName        text,
                contactId       text,
                photoUrl        text,
                photoFName      text,
                uploadDate      integer,
                downloadDate    integer,
                UNIQUE
                (
                    userName,
                    contactId,
                    photoUrl
                ) ON CONFLICT REPLACE
            )""")

            self.dbconn.commit()
        except Exception, e:
            traceback.print_exc()
            print >>sys.stderr, e
            self.dbconn = None
            self.cursor = None

    def isConnected(self):
        return self.dbconn and self.cursor

    def getUserList(self):
        if not self.isConnected():
            raise NotConnectedException(self.db)

        userList = []
        self.cursor.execute('SELECT DISTINCT userName FROM subscriptions')
        for row in self.cursor:
            userList.append(row[0])
        return userList

    def getSubscriptions(self, userName):
        if not self.isConnected():
            raise NotConnectedException(self.db)

        subscriptions = []
        self.cursor.execute('SELECT contactName, contactId FROM subscriptions WHERE userName=? AND subscribed=1', (userName,))
        for row in self.cursor:
            subscriptions.append( (row[0], row[1]) )
        return subscriptions

    def isSubscribed(self, userName, contactId):
        if not self.isConnected():
            raise NotConnectedException(self.db)

        subscribed = False
        self.cursor.execute('SELECT subscribed FROM subscriptions WHERE userName=? AND contactId=?', (userName, contactId))
        for row in self.cursor:
            subscribed = row[0]
        return subscribed

    def maxPhotos(self, userName, contactId):
        if not self.isConnected():
            raise NotConnectedException(self.db)

        maxPhotos = 0
        self.cursor.execute('SELECT maxPhotos FROM subscriptions WHERE userName=? AND contactId=?', (userName, contactId))
        for row in self.cursor:
            maxPhotos= row[0]
        return maxPhotos

    def getLastUploadDate(self, userName, contactId):
        if not self.isConnected():
            raise NotConnectedException(self.db)

        lastdate = None
        self.cursor.execute('SELECT MAX(uploadDate) FROM watch WHERE userName=? AND contactId=?', (userName, contactId))
        for row in self.cursor:
            lastdate = row[0]

        return lastdate

    def photoDownloaded(self, userName, contactId, photoUrl, uploadDate):
        if not self.isConnected():
            raise NotConnectedException(self.db)

        self.cursor.execute('SELECT uploadDate FROM watch WHERE userName=? AND contactId=? AND photoUrl=?', (userName, contactId, photoUrl))

        downloaded = False
        for row in self.cursor:
            if row[0] >= uploadDate:
                downloaded = True
                break

        return downloaded

    def markPhotoDownloaded(self, userName, contactId, photoUrl, photoFName, uploadDate):
        if not self.isConnected():
            raise NotConnectedException(self.db)

        self.cursor.execute('REPLACE INTO watch (userName, contactId, photoUrl, photoFName, uploadDate, downloadDate) VALUES (?, ?, ?, ?, ?, ?)',
                            (userName, contactId, photoUrl, photoFName, uploadDate, time.time()))
        self.dbconn.commit()
    
    def subscribe(self, userName, contactId, contactName, maxPhotos):
        if not self.isConnected():
            raise NotConnectedException(self.db)

        self.cursor.execute('REPLACE INTO subscriptions (userName, contactId, contactName, subscribed, maxPhotos) VALUES (?, ?, ?, 1, ?)',
                            (userName, contactId, contactName, maxPhotos))
        self.dbconn.commit()

    def unsubscribe(self, userName, contactId):
        if not self.isConnected():
            raise NotConnectedException(self.db)

        self.cursor.execute('REPLACE INTO subscriptions (userName, contactId, subscribed) VALUES (?, ?, 0)',
                            (userName, contactId))
        self.dbconn.commit()

    def getPhotoCount(self, userName, contactId):
        if not self.isConnected():
            raise NotConnectedException(self.db)

        photoCount = 0
        self.cursor.execute('SELECT COUNT(*) FROM watch WHERE userName=? AND contactId=?', (userName, contactId))
        for row in self.cursor:
            photoCount = row[0]

        return photoCount

    def getOldestPhotos(self, userName, contactId, photoCount):
        if not self.isConnected():
            raise NotConnectedException(self.db)

        oldestPhotos = []
        self.cursor.execute('SELECT photoFName, photoUrl FROM watch WHERE userName=? AND contactId=? ORDER BY uploadDate LIMIT ?',
                            (userName, contactId, photoCount))
        for row in self.cursor:
            oldestPhotos.append( (row[0], row[1]) )

        return oldestPhotos

    def removePhotos(self, userName, contactId, photoList):
        if not self.isConnected():
            raise NotConnectedException(self.db)

        for (photoFName, photoUrl) in photoList:
            self.cursor.execute('DELETE FROM TABLE watch WHERE userName=? AND contactId=? AND photoFName=? AND photoUrl=?',
                                (photoFName, photoUrl))
        self.dbconn.commit()

def download_photos(userName, contactName, contactId, maxPhotos, flickr, db):
    if not os.path.exists(userName+'/'+contactName):
        os.mkdir(userName+'/'+contactName)

    lastdate = db.getLastUploadDate(userName, contactId)

    if lastdate:
        photos  = flickr.photos_search(user_id=contactId, min_upload_date=lastdate, safe_search='3', extras='date_upload', per_page=maxPhotos)
    else:
        photos  = flickr.photos_search(user_id=contactId, safe_search='3', extras='date_upload', per_page=maxPhotos)

    if photos.photos[0]['pages'] == '0':
        return

    for photo in photos.photos[0].photo:
        photoUrl    = 'http://farm%(farm)s.static.flickr.com/%(server)s/%(id)s_%(secret)s.jpg' % photo
        fname       = '%(farm)s_%(server)s_%(id)s_%(secret)s.jpg' % photo

        uploadDate = photo['dateupload']

        if not db.photoDownloaded(userName, contactId, photoUrl, uploadDate):
            try:
                photod  = urllib2.urlopen(photoUrl)
                fd      = open(userName+'/'+contactName+'/'+fname, 'w')
                shutil.copyfileobj(photod, fd)
                photod.close()
                fd.close()

                db.markPhotoDownloaded(userName, contactId, photoUrl, fname, uploadDate)
            except:
                traceback.print_exc()
                print >>sys.stderr, "Download of", photoUrl, "failed."

    photoCount = db.getPhotoCount(userName, contactId)

    if photoCount > maxPhotos:
        oldestPhotos = db.getOldestPhotos(userName, contactId, photoCount - maxPhotos)
        for (fname, photoUrl) in oldestPhotos:
            os.remove(userName+'/'+contactName+'/'+fname)
        db.removePhotos(userName, contactId, oldestPhotos)

def getSubscribedPhotos(userName, flickr, db):
    subscriptions = db.getSubscriptions(userName)
    for (contactName, contactId) in subscriptions:
        # Download photos from subscribed contacts 
        maxPhotos = db.maxPhotos(userName, contactId)
        download_photos(userName, contactName, contactId, maxPhotos, flickr, db)

def subscribe(userName, userId, flickr, db):
    unsubscribed = []

    contacts  = flickr.contacts_getList(user_id=userId)
    for contact in contacts.contacts[0].contact:
        contactId   = contact['nsid']
        contactName = contact['username']

        if not db.isSubscribed(userName, contactId):
            unsubscribed.append( (contactName, contactId) )

    if not db.isSubscribed(userName, userId):
        unsubscribed.append( (userName, userId) )

    contact = None

    while not contact:
        print ""
        idx = 0
        for (contactName, contactId) in unsubscribed:
            print "%d) %s" % (idx, contactName)
            idx = idx + 1
        print "%d) Done subscribing" % (idx)

        print ""
        contact = raw_input('Subscribe to contact: ')

        try:
            contact = int(contact)
        except:
            print ""
            print "Invalid selection %s, retry 0-%d" % (contact, idx)
            contact = None
            continue

        if contact < 0 or contact > idx:
            print ""
            print "Invalid selection %d, retry 0-%d" % (contact, idx)
            contact = None
        elif contact == idx:
            break
        else:
            (contactName, contactId) = unsubscribed[contact]
            maxPhotos = None

            while not maxPhotos:
                print ""
                maxPhotos = raw_input('How many photos from %s do you want to keep (1-500)? ' % contactName)

                try:
                    maxPhotos = int(maxPhotos)
                except:
                    print ""
                    print "Invalid selection %s, retry 0-500" % (maxPhotos)
                    maxPhotos = None
                    continue

                if maxPhotos < 1 or maxPhotos > 500:
                    print ""
                    print "Invalid input %d, retry 1-500" % (maxPhotos)
                    maxPhotos = None

            db.subscribe(userName, contactId, contactName, maxPhotos)
            del unsubscribed[contact]
            contact = None
        

def unsubscribe(userName, userId, flickr, db):
    subscribed = db.getSubscriptions(userName)

    contact = None

    while not contact:
        print ""
        idx = 0
        for (contactName, contactId) in subscribed:
            print "%d) %s" % (idx, contactName)
            idx = idx + 1
        print "%d) Done unsubscribing" % (idx)

        print ""
        contact = raw_input('Unsubscribe to contact: ')

        try:
            contact = int(contact)
        except:
            print ""
            print "Invalid selection %s, retry 0-%d" % (contact, idx)
            contact = None
            continue

        if contact < 0 or contact > idx:
            print ""
            print "Invalid selection %d, retry 0-%d" % (contact, idx)
            contact = None
        elif contact == idx:
            break
        else:
            (contactName, contactId) = subscribed[contact]
            confirm = None

            while not confirm:
                print ""
                confirm = raw_input('Are you sure you want to unsubscribe to %s (y or n)? ' % contactName)[0].lower()
                if confirm != 'y' and confirm != n:
                    print ""
                    print "Invalid input %d, retry y or n" % (maxPhotos)
                    confirm = None

            if confirm == 'y':
                db.unsubscribe(userName, contactId)
            contact = None

def manageSubscriptions(userName, userId, flickr, db):
    choice      = {1: subscribe, 2: unsubscribe}
    selection   = 0
    while not selection:
        print ""
        print "1) Add subscriptions"
        print "2) Delete subscriptions"

        print ""
        selection = raw_input('Add or delete: ')

        try:
            selection = int(selection)
        except:
            print ""
            print "Invalid selection %s, retry 1-2" % (selection)
            selection = None
            continue

        if selection < 1 or selection > 2:
            print ""
            print "Invalid selection %d, retry 1-2" % (selection)

    choice[selection](userName, userId, flickr, db)
    

db = FlickrWatcherDb()

userList = db.getUserList()    
userName = None

while not userName:
    print ""
    idx = 0
    for user in userList:
        print "%d) %s" % (idx, user)
        idx = idx + 1

    print "%d) Enter new user" % (idx)
    print ""

    userNumber = raw_input('Enter user number: ')

    try:
        userNumber = int(userNumber)
    except:
        print ""
        print "Invalid selection %s, retry %d-%d" % (userNumber, 0, idx)
        continue
        
    if  userNumber == idx:
        userName = raw_input('Enter user name: ')
    elif userNumber > idx or userNumber < 0:
        print ""
        print "Invalid selection %d, retry %d-%d" % (userNumber, 0, idx)
    else:
        userName = userList[userNumber]

if not os.path.exists(userName):
    os.mkdir(userName)

flickr          = flickrapi.FlickrAPI(apiKey, apiSecret)
(token, frob)   = flickr.getTokenPartOne(perms='read')

if not token:
    raw_input("Press ENTER after you authorized this program.")

flickr.getTokenPartTwo((token, frob))

user        = flickr.people_findByUsername(username=userName)
userId      = user.user[0]['nsid']

while True:
    selection = None

    while not selection:
        print ""
        print "1) Manage subscriptions"
        print "2) Get new photos"
        print "3) Exit"
        print ""

        selection = raw_input('What do you want to do?: ')

        try:
            selection = int(selection)
        except:
            print ""
            print "Invalid selection %s, retry 1-3" % (selection)
            selection = None
            continue

        if selection < 1 or selection > 3:
            print ""
            print "Invalid selection %d, retry 1-3" % (selection)
            selection = None
            continue

    if      selection == 1:
        manageSubscriptions(userName, userId, flickr, db)
    elif    selection == 2:
        getSubscribedPhotos(userName, flickr, db)
    elif    selection == 3:
        break
